package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
 * 聊天室服务端
 */
public class Server_v4 {
    /*
        java.net.ServerSocket 运行在服务端的ServerSocket主要有两个作用
        1:打开服务端口，客户端就是通过这个端口与服务端建立连接的。本案例打开的就是8088端口
        2:监听服务端口(8088),一旦一个客户端通过该端口与服务端请求建立连接，此时会立即返回
          一个Socket实例，服务端就通过该实例与该客户端交互。

        将ServerSocket比喻为"总机"。客户端的"电话"都是先拨通到"总机"上，然后"总机"分配一台"电话"，
        通过这个"电话"就可以和该客户端沟通了
     */
    private ServerSocket serverSocket;

    //该数组存放所有客户端的输出流，用于广播消息给所有客户端
//    private PrintWriter[] allOut = {};
    private Collection<PrintWriter> allOut = new ArrayList<>();

    public Server_v4(){
        try {
            System.out.println("正在启动服务端，打开8088端口...");
            /*
                这里实例化ServerSocket的同时我们要指定服务端口，客户端就是通过该端口连接的
                注意:这个端口不能与操作系统上其他应用程序已经打开的端口重复，否则会抛出异常:
                java.net.BindException:address already in use
                         绑定异常       地址     已经     被使用
                解决办法:更换端口，直到可用。
                       或者杀死之前运行的服务端程序.
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接...");
                /*
                ServerSocket提供的一个重要方法:
                Socket accept()
                该方法用于接收客户端的连接，该方法是一个阻塞方法，调用后程序会"卡住"，直到一个
                客户端连接了ServerSocket,此时该方法会立即返回一个Socket实例。服务端通过该
                实例就可以与连接的客户端交互了。
                该方法理解为就是总机的"接电话"动作。
             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程负责与该客户端交互
                ClientHandler clientHandler = new ClientHandler(socket);
                Thread t = new Thread(clientHandler);
                t.start();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        Server_v4 server = new Server_v4();
        server.start();
    }

    /**
     * 该线程任务负责与指定的客户端进行交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录该客户端的IP地址信息
        public ClientHandler(Socket socket){
           this.socket = socket;
           //通过socket获取远端计算机的地址信息
           host = socket.getInetAddress().getHostAddress();
        }
        public void run() {
            PrintWriter pw = null;
            try {
                /*
                Socket一个重要方法:
                InputStream getInputStream()
                通过该方法获取的字节输入流可以读取来自远端计算机发送过来的数据
             */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流，用于将消息发送给该客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw
                        = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);
                //将输出流存入到共享数组中

                /*
                    这里指定this作为同步监视器对象不可以
                    原因在于不同的线程都有自己的线程任务(ClientHandler实例)
                    而每个ClientHandler的run方法中的this就是该实例,因此多个
                    线程看到的不是同一个对象!
                 */
//                synchronized (this) {
                /*
                    通常情况下当多个线程并发访问同一个临界资源时,我们就将该临界
                    资源作为同步监视器对象即可!
                    这里可以直接将集合对象allOut作为同步监视器对象
                 */
                synchronized (allOut) {
                    allOut.add(pw);
                }

                //通知所有客户端该用户上线了
                sendMessage(host+"上线了,当前在线人数:"+allOut.size());

                /*
                    如果客户端异常断开，服务端会抛出异常
                    java.net.SocketException: Connection reset
                    该异常无法避免，因为这个是客户端的操作导致的。
                 */
                String line;
                while ((line = br.readLine()) != null) {
                    sendMessage(host + "说:" + line);
                }

            }catch(IOException e){
                e.printStackTrace();
            }finally {
                //处理该客户端下线后的操作
                //1将该客户端的输出流从共享集合allOut中删除
                synchronized (allOut){
                    allOut.remove(pw);
                }

                sendMessage(host+"下线了,当前在线人数:"+allOut.size());

                //2将对应该客户端的socket关闭;
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //将消息广播给所有客户端
        private void sendMessage(String message){
            System.out.println(message);
            //将消息发送给所有客户端
            synchronized (allOut) {
                for (PrintWriter pw :allOut) {
                    pw.println(message);
                }
            }
        }

    }
}
